// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo
// ReSharper disable InconsistentNaming

/* Axis.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.Serialization;

#endregion

#nullable enable

namespace AM.Drawing.Charting;

/// <summary>
/// The Axis class is an abstract base class that encompasses all properties
/// and methods required to define a graph Axis.
/// </summary>
/// <remarks>This class is inherited by the
/// <see cref="XAxis"/>, <see cref="YAxis"/>, and <see cref="Y2Axis"/> classes
/// to define specific characteristics for those types.
/// </remarks>
[Serializable]
public abstract class Axis
    : ISerializable, ICloneable
{
    #region Class Fields

    /// <summary>
    /// Private field that stores the <see cref="Charting.MinorTic" /> class, which handles all
    /// the minor tic information.  See the public property <see cref="MinorTic" /> to access this class.
    /// </summary>
    internal MinorTic _minorTic;

    /// <summary>
    /// Private field that stores the <see cref="Charting.MajorTic" /> class, which handles all
    /// the major tic information.  See the public property <see cref="MajorTic" /> to access this class.
    /// </summary>
    internal MajorTic _majorTic;

    /// <summary>
    /// Private field that stores the <see cref="Charting.MajorGrid" /> class, which handles all
    /// the major grid information.  See the public property <see cref="MajorGrid" /> to access this class.
    /// </summary>
    internal MajorGrid _majorGrid;

    /// <summary>
    /// Private field that stores the <see cref="Charting.MinorGrid" /> class, which handles all
    /// the minor grid information.  See the public property <see cref="MinorGrid" /> to access this class.
    /// </summary>
    internal MinorGrid _minorGrid;

    /// <summary> Private fields for the <see cref="Axis"/> scale rendering properties.
    /// Use the public properties <see cref="Cross"/> and <see cref="Charting.Scale.BaseTic"/>
    /// for access to these values.
    /// </summary>
    internal double _cross;

    /// <summary>
    /// A tag object for use by the user.  This can be used to store additional
    /// information associated with the <see cref="Axis"/>.  ZedGraph does
    /// not use this value for any purpose.
    /// </summary>
    /// <remarks>
    /// Note that, if you are going to Serialize ZedGraph data, then any type
    /// that you store in <see cref="Tag"/> must be a serializable type (or
    /// it will cause an exception).
    /// </remarks>
    public object? Tag;

    /// <summary> Private field for the <see cref="Axis"/> drawing dimensions.
    /// Use the public property <see cref="AxisGap"/>
    /// for access to these values. </summary>
    private float _axisGap;

    /// <summary>
    /// Private field for the <see cref="Axis"/> minimum allowable space allocation.
    /// Use the public property <see cref="MinSpace"/> to access this value.
    /// </summary>
    /// <seealso cref="Default.MinSpace"/>
    private float _minSpace;

    /// <summary> Private fields for the <see cref="Axis"/> colors.
    /// Use the public property <see cref="Color"/> for access to this values.
    /// </summary>
    private Color _color;

    /// <summary>
    /// Temporary values for axis space calculations (see <see cref="CalcSpace" />).
    /// </summary>
    internal float _tmpSpace;

    #endregion

    #region Events

    /// <summary>
    /// A delegate that allows full custom formatting of the Axis labels
    /// </summary>
    /// <param name="pane">The <see cref="GraphPane" /> for which the label is to be
    /// formatted</param>
    /// <param name="axis">The <see cref="Scale" /> of interest.</param>
    /// <param name="val">The value to be formatted</param>
    /// <param name="index">The zero-based index of the label to be formatted</param>
    /// <returns>
    /// A string value representing the label, or null if the ZedGraph should go ahead
    /// and generate the label according to the current settings</returns>
    /// <seealso cref="ScaleFormatEvent" />
    public delegate string ScaleFormatHandler (GraphPane pane, Axis axis, double val, int index);

    /// <summary>
    /// Subscribe to this event to handle custom formatting of the scale labels.
    /// </summary>
    public event ScaleFormatHandler? ScaleFormatEvent;

    /// <summary>
    /// Allow customization of title based on user preferences.
    /// </summary>
    /// <param name="axis">The <see cref="Axis" /> of interest.</param>
    /// <returns>
    /// A string value representing the label, or null if the ZedGraph should go ahead
    /// and generate the label according to the current settings.  To make the title
    /// blank, return "".</returns>
    /// <seealso cref="ScaleFormatEvent" />
    public delegate string ScaleTitleEventHandler (Axis axis);

    /// <summary>
    /// Allow customization of the title when the scale is very large
    /// Subscribe to this event to handle custom formatting of the scale axis label.
    /// </summary>
    public event ScaleTitleEventHandler? ScaleTitleEvent;

    #endregion

    #region Defaults

    /// <summary>
    /// A simple struct that defines the
    /// default property values for the <see cref="Axis"/> class.
    /// </summary>
    public struct Default
    {
        /// <summary>
        /// The default size for the gap between multiple axes
        /// (<see cref="Axis.AxisGap"/> property). Units are in points (1/72 inch).
        /// </summary>
        public static float AxisGap = 5;

        /// <summary>
        /// The default setting for the gap between the scale labels and the axis title.
        /// </summary>
        public static float TitleGap = 0.0f;

        /// <summary>
        /// The default font family for the <see cref="Axis"/> <see cref="Title" /> text
        /// font specification <see cref="FontSpec"/>
        /// (<see cref="FontSpec.Family"/> property).
        /// </summary>
        public static string TitleFontFamily = "Arial";

        /// <summary>
        /// The default font size for the <see cref="Axis"/> <see cref="Title" /> text
        /// font specification <see cref="FontSpec"/>
        /// (<see cref="FontSpec.Size"/> property).  Units are
        /// in points (1/72 inch).
        /// </summary>
        public static float TitleFontSize = 14;

        /// <summary>
        /// The default font color for the <see cref="Axis"/> <see cref="Title" /> text
        /// font specification <see cref="FontSpec"/>
        /// (<see cref="FontSpec.FontColor"/> property).
        /// </summary>
        public static Color TitleFontColor = Color.Black;

        /// <summary>
        /// The default font bold mode for the <see cref="Axis"/> <see cref="Title" /> text
        /// font specification <see cref="FontSpec"/>
        /// (<see cref="FontSpec.IsBold"/> property). true
        /// for a bold typeface, false otherwise.
        /// </summary>
        public static bool TitleFontBold = true;

        /// <summary>
        /// The default font italic mode for the <see cref="Axis"/> <see cref="Title" /> text
        /// font specification <see cref="FontSpec"/>
        /// (<see cref="FontSpec.IsItalic"/> property). true
        /// for an italic typeface, false otherwise.
        /// </summary>
        public static bool TitleFontItalic = false;

        /// <summary>
        /// The default font underline mode for the <see cref="Axis"/> <see cref="Title" /> text
        /// font specification <see cref="FontSpec"/>
        /// (<see cref="FontSpec.IsUnderline"/> property). true
        /// for an underlined typeface, false otherwise.
        /// </summary>
        public static bool TitleFontUnderline = false;

        /// <summary>
        /// The default color for filling in the <see cref="Title" /> text background
        /// (see <see cref="Fill.Color"/> property).
        /// </summary>
        public static Color TitleFillColor = Color.White;

        /// <summary>
        /// The default custom brush for filling in the <see cref="Title" /> text background
        /// (see <see cref="Fill.Brush"/> property).
        /// </summary>
        public static Brush? TitleFillBrush = null;

        /// <summary>
        /// The default fill mode for filling in the <see cref="Title" /> text background
        /// (see <see cref="Fill.Type"/> property).
        /// </summary>
        public static FillType TitleFillType = FillType.None;

        /// <summary>
        /// The default color for the <see cref="Axis"/> itself
        /// (<see cref="Axis.Color"/> property).  This color only affects the
        /// the axis border.
        /// </summary>
        public static Color BorderColor = Color.Black;

        /// <summary>
        /// The default value for <see cref="Axis.IsAxisSegmentVisible"/>, which determines
        /// whether or not the scale segment itself is visible
        /// </summary>
        public static bool IsAxisSegmentVisible = true;

        /// <summary>
        /// The default setting for the <see cref="Axis"/> scale axis type
        /// (<see cref="Axis.Type"/> property).  This value is set as per
        /// the <see cref="AxisType"/> enumeration
        /// </summary>
        public static AxisType Type = AxisType.Linear;

        /// <summary>
        /// The default color for the axis segment.
        /// </summary>
        public static Color Color = Color.Black;

        /// <summary>
        /// The default setting for the axis space allocation.  This term, expressed in
        /// points (1/72 inch) and scaled according to <see cref="PaneBase.CalcScaleFactor"/> for the
        /// <see cref="GraphPane"/>, determines the minimum amount of space an axis must
        /// have between the <see cref="Chart.Rect"/> and the
        /// <see cref="PaneBase.Rect"/>.  This minimum space
        /// applies whether <see cref="Axis.IsVisible"/> is true or false.
        /// </summary>
        public static float MinSpace = 0f;
    }

    #endregion

    #region Constructors

    /// <summary>
    /// Default constructor for <see cref="Axis"/> that sets all axis properties
    /// to default values as defined in the <see cref="Default"/> class.
    /// </summary>
    protected Axis()
    {
        Scale = new LinearScale (this);

        _cross = 0.0;

        CrossAuto = true;

        _majorTic = new MajorTic();
        _minorTic = new MinorTic();

        _majorGrid = new MajorGrid();
        _minorGrid = new MinorGrid();

        _axisGap = Default.AxisGap;

        _minSpace = Default.MinSpace;
        IsVisible = true;

        IsAxisSegmentVisible = Default.IsAxisSegmentVisible;

        Title = new AxisLabel
            (
                string.Empty,
                Default.TitleFontFamily,
                Default.TitleFontSize,
                Default.TitleFontColor,
                Default.TitleFontBold,
                Default.TitleFontUnderline,
                Default.TitleFontItalic
            );
        Title.FontSpec!.Fill = new Fill
            (
                Default.TitleFillColor,
                Default.TitleFillBrush,
                Default.TitleFillType
            );

        Title.FontSpec.Border.IsVisible = false;


        _color = Default.Color;
    }

    /// <summary>
    /// Constructor for <see cref="Axis"/> that sets all axis properties
    /// to default values as defined in the <see cref="Default"/> class,
    /// except for the <see cref="Title"/>.
    /// </summary>
    /// <param name="title">A string containing the axis title</param>
    protected Axis (string title)
        : this()
    {
        Title.Text = title;
    }

    /// <summary>
    /// The Copy Constructor.
    /// </summary>
    /// <param name="rhs">The Axis object from which to copy</param>
    protected Axis (Axis rhs)
    {
        Scale = rhs.Scale?.Clone (this);

        _cross = rhs._cross;

        CrossAuto = rhs.CrossAuto;

        _majorTic = rhs.MajorTic.Clone();
        _minorTic = rhs.MinorTic.Clone();

        _majorGrid = rhs._majorGrid.Clone();
        _minorGrid = rhs._minorGrid.Clone();

        IsVisible = rhs.IsVisible;

        IsAxisSegmentVisible = rhs.IsAxisSegmentVisible;

        Title = rhs.Title.Clone();

        _axisGap = rhs._axisGap;

        _minSpace = rhs.MinSpace;

        _color = rhs.Color;
    }

    /// <inheritdoc cref="ICloneable.Clone"/>
    object ICloneable.Clone()
    {
        throw new NotImplementedException ("Can't clone an abstract base type -- child types must implement ICloneable");

        //return new PaneBase( this );
    }

    #endregion

    #region Serialization

    /// <summary>
    /// Current schema value that defines the version of the serialized file
    /// </summary>
    public const int schema = 10;

    /// <summary>
    /// Constructor for deserializing objects
    /// </summary>
    /// <param name="info">A <see cref="SerializationInfo"/> instance that defines the serialized data
    /// </param>
    /// <param name="context">A <see cref="StreamingContext"/> instance that contains the serialized data
    /// </param>
    protected Axis
        (
            SerializationInfo info,
            StreamingContext context
        )
    {
        // The schema value is just a file version parameter.  You can use it to make future versions
        // backwards compatible as new member variables are added to classes
        info.GetInt32 ("schema").NotUsed();

        _cross = info.GetDouble ("cross");
        CrossAuto = info.GetBoolean ("crossAuto");

        _majorTic = (MajorTic) info.GetValue ("MajorTic", typeof (MajorTic)).ThrowIfNull();
        _minorTic = (MinorTic) info.GetValue ("MinorTic", typeof (MinorTic)).ThrowIfNull();
        _majorGrid = (MajorGrid) info.GetValue ("majorGrid", typeof (MajorGrid)).ThrowIfNull();
        _minorGrid = (MinorGrid) info.GetValue ("minorGrid", typeof (MinorGrid)).ThrowIfNull();

        IsVisible = info.GetBoolean ("isVisible");

        Title = (AxisLabel) info.GetValue ("title", typeof (AxisLabel)).ThrowIfNull();

        _minSpace = info.GetSingle ("minSpace");

        _color = (Color) info.GetValue ("color", typeof (Color)).ThrowIfNull();

        IsAxisSegmentVisible = info.GetBoolean ("isAxisSegmentVisible");


        _axisGap = info.GetSingle ("axisGap");

        Scale = (Scale) info.GetValue ("scale", typeof (Scale)).ThrowIfNull();
        Scale._ownerAxis = this;
    }

    /// <summary>
    /// Populates a <see cref="SerializationInfo"/> instance with the data needed to serialize the target object
    /// </summary>
    /// <param name="info">A <see cref="SerializationInfo"/> instance that defines the serialized data</param>
    /// <param name="context">A <see cref="StreamingContext"/> instance that contains the serialized data</param>
    public virtual void GetObjectData
        (
            SerializationInfo info,
            StreamingContext context
        )
    {
        info.AddValue ("schema", schema);

        info.AddValue ("cross", _cross);
        info.AddValue ("crossAuto", CrossAuto);

        info.AddValue ("MajorTic", MajorTic);
        info.AddValue ("MinorTic", MinorTic);
        info.AddValue ("majorGrid", _majorGrid);
        info.AddValue ("minorGrid", _minorGrid);

        info.AddValue ("isVisible", IsVisible);
        info.AddValue ("title", Title);
        info.AddValue ("minSpace", _minSpace);
        info.AddValue ("color", _color);
        info.AddValue ("isAxisSegmentVisible", IsAxisSegmentVisible);
        info.AddValue ("axisGap", _axisGap);
        info.AddValue ("scale", Scale);
    }

    #endregion

    #region Scale Properties

    /// <summary>
    /// Gets the <see cref="Scale" /> instance associated with this <see cref="Axis" />.
    /// </summary>
    public Scale? Scale { get; internal set; }

    /// <summary>
    /// Gets or sets the scale value at which this axis should cross the "other" axis.
    /// </summary>
    /// <remarks>This property allows the axis to be shifted away from its default location.
    /// For example, for a graph with an X range from -100 to +100, the Y Axis can be located
    /// at the X=0 value rather than the left edge of the ChartRect.  This value can be set
    /// automatically based on the state of <see cref="CrossAuto"/>.  If
    /// this value is set manually, then <see cref="CrossAuto"/> will
    /// also be set to false.  The "other" axis is the axis the handles the second dimension
    /// for the graph.  For the XAxis, the "other" axis is the YAxis.  For the YAxis or
    /// Y2Axis, the "other" axis is the XAxis.
    /// </remarks>
    /// <value> The value is defined in user scale units </value>
    /// <seealso cref="Charting.Scale.Min"/>
    /// <seealso cref="Charting.Scale.Max"/>
    /// <seealso cref="Charting.Scale.MajorStep"/>
    /// <seealso cref="CrossAuto"/>
    public double Cross
    {
        get => _cross;
        set
        {
            _cross = value;
            CrossAuto = false;
        }
    }

    /// <summary>
    /// Gets or sets a value that determines whether or not the <see cref="Cross"/> value
    /// is set automatically.
    /// </summary>
    /// <value>Set to true to have ZedGraph put the axis in the default location, or false
    /// to specify the axis location manually with a <see cref="Cross"/> value.</value>
    /// <seealso cref="Charting.Scale.Min"/>
    /// <seealso cref="Charting.Scale.Max"/>
    /// <seealso cref="Charting.Scale.MajorStep"/>
    /// <seealso cref="Cross"/>
    public bool CrossAuto { get; set; }

    /// <summary>
    /// Gets or sets the minimum axis space allocation.
    /// </summary>
    /// <remarks>
    /// This term, expressed in
    /// points (1/72 inch) and scaled according to <see cref="PaneBase.CalcScaleFactor"/>
    /// for the <see cref="GraphPane"/>, determines the minimum amount of space
    /// an axis must have between the <see cref="Chart.Rect">Chart.Rect</see> and the
    /// <see cref="PaneBase.Rect">GraphPane.Rect</see>.  This minimum space
    /// applies whether <see cref="IsVisible"/> is true or false.
    /// </remarks>
    public float MinSpace
    {
        get => _minSpace;
        set => _minSpace = value;
    }

    #endregion

    #region Tic Properties

    /// <summary>
    /// The color to use for drawing this <see cref="Axis"/>.
    /// </summary>
    /// <remarks>
    /// This affects only the axis segment (see <see cref="IsAxisSegmentVisible" />),
    /// since the <see cref="Title"/>,
    /// <see cref="Scale"/>, <see cref="MajorTic" />, <see cref="MinorTic" />,
    /// <see cref="MajorGrid" />, and <see cref="MinorGrid" />
    /// all have their own color specification.
    /// </remarks>
    /// <value> The color is defined using the
    /// <see cref="System.Drawing.Color"/> class</value>
    /// <seealso cref="Default.Color"/>.
    /// <seealso cref="IsVisible"/>
    public Color Color
    {
        get => _color;
        set => _color = value;
    }

    /// <summary>
    /// Gets a reference to the <see cref="Charting.MajorTic" /> class instance
    /// for this <see cref="Axis" />.  This class stores all the major tic settings.
    /// </summary>
    public MajorTic MajorTic => _majorTic;

    /// <summary>
    /// Gets a reference to the <see cref="Charting.MinorTic" /> class instance
    /// for this <see cref="Axis" />.  This class stores all the minor tic settings.
    /// </summary>
    public MinorTic MinorTic => _minorTic;

    #endregion

    #region Grid Properties

    /// <summary>
    /// Gets a reference to the <see cref="MajorGrid" /> class that contains the properties
    /// of the major grid.
    /// </summary>
    public MajorGrid MajorGrid => _majorGrid;

    /// <summary>
    /// Gets a reference to the <see cref="MinorGrid" /> class that contains the properties
    /// of the minor grid.
    /// </summary>
    public MinorGrid MinorGrid => _minorGrid;

    #endregion

    #region Type Properties

    /// <summary>
    /// This property determines whether or not the <see cref="Axis"/> is shown.
    /// </summary>
    /// <remarks>
    /// Note that even if
    /// the axis is not visible, it can still be actively used to draw curves on a
    /// graph, it will just be invisible to the user
    /// </remarks>
    /// <value>true to show the axis, false to disable all drawing of this axis</value>
    /// <seealso cref="Charting.Scale.IsVisible"/>.
    /// <seealso cref="XAxis.Default.IsVisible"/>.
    /// <seealso cref="YAxis.Default.IsVisible"/>.
    /// <seealso cref="Y2Axis.Default.IsVisible"/>.
    public bool IsVisible { get; set; }

    /// <summary>
    /// Gets or sets a property that determines whether or not the axis segment (the line that
    /// represents the axis itself) is drawn.
    /// </summary>
    /// <remarks>
    /// Under normal circumstances, this value won't affect the appearance of the display because
    /// the Axis segment is overlain by the Axis border (see <see cref="Chart.Border"/>).
    /// However, when the border is not visible, or when <see cref="Axis.CrossAuto"/> is set to
    /// false, this value will make a difference.
    /// </remarks>
    public bool IsAxisSegmentVisible { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="AxisType"/> for this <see cref="Axis"/>.
    /// </summary>
    /// <remarks>
    /// The type can be either <see cref="AxisType.Linear"/>,
    /// <see cref="AxisType.Log"/>, <see cref="AxisType.Date"/>,
    /// or <see cref="AxisType.Text"/>.
    /// </remarks>
    /// <seealso cref="Charting.Scale.IsLog"/>
    /// <seealso cref="Charting.Scale.IsText"/>
    /// <seealso cref="Charting.Scale.IsOrdinal"/>
    /// <seealso cref="Charting.Scale.IsDate"/>
    /// <seealso cref="Charting.Scale.IsReverse"/>
    public AxisType Type
    {
        get => Scale.ThrowIfNull().Type;
        set
        {
            var scale = Scale.ThrowIfNull();
            Scale = scale.MakeNewScale (scale, value);
        }
    }

    #endregion

    #region Label Properties

    /// <summary>
    /// Gets or sets the <see cref="Label" /> class that contains the title of this
    /// <see cref="Axis"/>.
    /// </summary>
    /// <remarks>The title normally shows the basis and dimensions of
    /// the scale range, such as "Time (Years)".  The title is only shown if the
    /// <see cref="Label.IsVisible"/> property is set to true.  If the Title text is empty,
    /// then no title is shown, and no space is "reserved" for the title on the graph.
    /// </remarks>
    /// <value>the title is a string value</value>
    /// <seealso cref="AxisLabel.IsOmitMag"/>
    public AxisLabel Title { get; set; }

    /// <summary>
    /// The size of the gap between multiple axes (see <see cref="GraphPane.YAxisList" /> and
    /// <see cref="GraphPane.Y2AxisList" />).
    /// </summary>
    /// <remarks>
    /// This size will be scaled
    /// according to the <see cref="PaneBase.CalcScaleFactor"/> for the
    /// <see cref="GraphPane"/>
    /// </remarks>
    /// <value>The axis gap is measured in points (1/72 inch)</value>
    /// <seealso cref="Default.AxisGap"/>.
    public float AxisGap
    {
        get => _axisGap;
        set => _axisGap = value;
    }

    #endregion

    #region Rendering Methods

    /// <summary>
    /// Restore the scale ranging to automatic mode, and recalculate the
    /// <see cref="Axis"/> scale ranges
    /// </summary>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    /// <param name="graphics">
    /// A graphic device object to be drawn into.  This is normally e.Graphics from the
    /// PaintEventArgs argument to the Paint() method.
    /// </param>
    /// <seealso cref="Charting.Scale.MinAuto"/>
    /// <seealso cref="Charting.Scale.MaxAuto"/>
    /// <seealso cref="Charting.Scale.MajorStepAuto"/>
    /// <seealso cref="Charting.Scale.MagAuto"/>
    /// <seealso cref="Charting.Scale.FormatAuto"/>
    public void ResetAutoScale
        (
            GraphPane pane,
            Graphics graphics
        )
    {
        var scale = Scale.ThrowIfNull();
        scale._minAuto = true;
        scale._maxAuto = true;
        scale._majorStepAuto = true;
        scale._minorStepAuto = true;
        CrossAuto = true;
        scale._magAuto = true;

        //this.numDecAuto = true;
        scale._formatAuto = true;
        pane.AxisChange (graphics);
    }

    /// <summary>
    /// Do all rendering associated with this <see cref="Axis"/> to the specified
    /// <see cref="Graphics"/> device.
    /// </summary>
    /// <remarks>
    /// This method is normally only
    /// called by the Draw method of the parent <see cref="GraphPane"/> object.
    /// </remarks>
    /// <param name="graphics">
    /// A graphic device object to be drawn into.  This is normally e.Graphics from the
    /// PaintEventArgs argument to the Paint() method.
    /// </param>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    /// <param name="scaleFactor">
    /// The scaling factor to be used for rendering objects.  This is calculated and
    /// passed down by the parent <see cref="GraphPane"/> object using the
    /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust
    /// font sizes, etc. according to the actual size of the graph.
    /// </param>
    /// <param name="shiftPos">
    /// The number of pixels to shift to account for non-primary axis position (e.g.,
    /// the second, third, fourth, etc. <see cref="YAxis" /> or <see cref="Y2Axis" />.
    /// </param>
    public void Draw
        (
            Graphics graphics,
            GraphPane pane,
            float scaleFactor,
            float shiftPos
        )
    {
        var saveMatrix = graphics.Transform;

        var scale = Scale.ThrowIfNull();
        scale.SetupScaleData (pane, this);
        if (IsVisible)
        {
            var smode = graphics.SmoothingMode;
            graphics.SmoothingMode = SmoothingMode.None;
            SetTransformMatrix (graphics, pane, scaleFactor);
            shiftPos = CalcTotalShift (pane, scaleFactor, shiftPos);
            scale.Draw (graphics, pane, scaleFactor, shiftPos);

            //DrawTitle( g, pane, scaleFactor );

            graphics.Transform = saveMatrix;
            graphics.SmoothingMode = smode;
        }
    }

    internal void DrawGrid
        (
            Graphics graphics,
            GraphPane pane,
            float scaleFactor,
            float shiftPos
        )
    {
        if (IsVisible)
        {
            var saveMatrix = graphics.Transform;
            var scale = Scale.ThrowIfNull();
            SetTransformMatrix (graphics, pane, scaleFactor);

            var baseVal = scale.CalcBaseTic();
            scale.GetTopRightPix (pane, out var topPix, out _);
            shiftPos = CalcTotalShift (pane, scaleFactor, shiftPos);
            scale.DrawGrid (graphics, pane, baseVal, topPix, scaleFactor);
            DrawMinorTics (graphics, pane, baseVal, shiftPos, scaleFactor, topPix);
            graphics.Transform = saveMatrix;
        }
    }

    /// <summary>
    /// This method will set the <see cref="MinSpace"/> property for this <see cref="Axis"/>
    /// using the currently required space multiplied by a fraction (<paramref>bufferFraction</paramref>).
    /// </summary>
    /// <remarks>
    /// The currently required space is calculated using <see cref="CalcSpace"/>, and is
    /// based on current data ranges, font sizes, etc.  The "space" is actually the amount of space
    /// required to fit the tic marks, scale labels, and axis title.
    /// </remarks>
    /// <param name="graphics">A graphic device object to be drawn into.  This is normally e.Graphics from the
    /// PaintEventArgs argument to the Paint() method.</param>
    /// <param name="pane">A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.</param>
    /// <param name="bufferFraction">The amount of space to allocate for the axis, expressed
    /// as a fraction of the currently required space.  For example, a value of 1.2 would
    /// allow for 20% extra above the currently required space.</param>
    /// <param name="isGrowOnly">If true, then this method will only modify the <see cref="MinSpace"/>
    /// property if the calculated result is more than the current value.</param>
    public void SetMinSpaceBuffer
        (
            Graphics graphics,
            GraphPane pane,
            float bufferFraction,
            bool isGrowOnly
        )
    {
        // save the original value of minSpace
        var oldSpace = MinSpace;

        // set minspace to zero, since we don't want it to affect the CalcSpace() result
        MinSpace = 0;

        // Calculate the space required for the current graph assuming scalefactor = 1.0
        // and apply the bufferFraction
        var space = CalcSpace (graphics, pane, 1.0F, out _) * bufferFraction;

        // isGrowOnly indicates the minSpace can grow but not shrink
        if (isGrowOnly)
        {
            space = Math.Max (oldSpace, space);
        }

        // Set the minSpace
        MinSpace = space;
    }

    /// <summary>
    /// Setup the Transform Matrix to handle drawing of this <see cref="Axis"/>
    /// </summary>
    /// <param name="graphics">
    /// A graphic device object to be drawn into.  This is normally e.Graphics from the
    /// PaintEventArgs argument to the Paint() method.
    /// </param>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    /// <param name="scaleFactor">
    /// The scaling factor to be used for rendering objects.  This is calculated and
    /// passed down by the parent <see cref="GraphPane"/> object using the
    /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust
    /// font sizes, etc. according to the actual size of the graph.
    /// </param>
    public abstract void SetTransformMatrix (Graphics graphics, GraphPane pane, float scaleFactor);


    /// <summary>
    /// Calculate the "shift" size, in pixels, in order to shift the axis from its default
    /// location to the value specified by <see cref="Cross"/>.
    /// </summary>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    /// <returns>The shift amount measured in pixels</returns>
    internal abstract float CalcCrossShift (GraphPane pane);

    /// <summary>
    /// Gets the "Cross" axis that corresponds to this axis.
    /// </summary>
    /// <remarks>
    /// The cross axis is the axis which determines the of this Axis when the
    /// <see cref="Axis.Cross" >Axis.Cross</see> property is used.  The
    /// cross axis for any <see cref="XAxis" /> or <see cref="X2Axis" />
    /// is always the primary <see cref="YAxis" />, and
    /// the cross axis for any <see cref="YAxis" /> or <see cref="Y2Axis" /> is
    /// always the primary <see cref="XAxis" />.
    /// </remarks>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    public abstract Axis GetCrossAxis (GraphPane pane);

//		abstract internal float GetMinPix( GraphPane pane );

    //abstract internal float CalcCrossFraction( GraphPane pane );

    /// <summary>
    /// Returns the linearized actual cross position for this axis, reflecting the settings of
    /// <see cref="Cross" />, <see cref="CrossAuto" />, and <see cref="Charting.Scale.IsReverse" />.
    /// </summary>
    /// <remarks>
    /// If the value of <see cref="Cross" /> lies outside the axis range, it is
    /// limited to the axis range.
    /// </remarks>
    internal double EffectiveCrossValue
        (
            GraphPane pane
        )
    {
        var crossAxis = GetCrossAxis (pane);
        var crossAxisScale = crossAxis.Scale.ThrowIfNull();

        // Use Linearize here instead of _minLinTemp because this method is called
        // as part of CalcRect() before scale is fully setup
        var min = crossAxisScale.Linearize (crossAxisScale._min);
        var max = crossAxisScale.Linearize (crossAxisScale._max);

        if (CrossAuto)
        {
            return crossAxisScale.IsReverse == (this is Y2Axis || this is X2Axis)
                ? max
                : min;
        }

        if (_cross < min)
        {
            return min;
        }
        if (_cross > max)
        {
            return max;
        }

        var scale = Scale.ThrowIfNull();

        return scale.Linearize (_cross);
    }

    /// <summary>
    /// Returns true if the axis is shifted at all due to the setting of
    /// <see cref="Cross" />.  This function will always return false if
    /// <see cref="CrossAuto" /> is true.
    /// </summary>
    internal bool IsCrossShifted (GraphPane pane)
    {
        if (CrossAuto)
        {
            return false;
        }

        var crossAxis = GetCrossAxis (pane);
        var crossAxisScale = crossAxis.Scale.ThrowIfNull();

        if ((this is XAxis || this is YAxis) && !crossAxisScale.IsReverse ||
            (this is X2Axis || this is Y2Axis) && crossAxisScale.IsReverse)
        {
            if (_cross <= crossAxisScale._min)
            {
                return false;
            }
        }
        else
        {
            if (_cross >= crossAxisScale._max)
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Calculates the proportional fraction of the total cross axis width at which
    /// this axis is located.
    /// </summary>
    /// <param name="pane"></param>
    /// <returns></returns>
    internal float CalcCrossFraction (GraphPane pane)
    {
        // if this axis is not shifted due to the Cross value
        if (!IsCrossShifted (pane))
        {
            // if it's the primary axis and the scale labels are on the inside, then we
            // don't need to save any room for the axis labels (they will be inside the chart rect)
            if (IsPrimary (pane) && (Scale?._isLabelsInside ?? false))
            {
                return 1.0f;
            }

            // otherwise, it's a secondary (outboard) axis and we always save room for the axis and labels.

            return 0.0f;
        }

        var effCross = EffectiveCrossValue (pane);
        var crossAxis = GetCrossAxis (pane);
        var crossAxisScale = crossAxis.Scale.ThrowIfNull();

        // Use Linearize here instead of _minLinTemp because this method is called
        // as part of CalcRect() before scale is fully setup
        //			double max = crossAxis._scale._maxLinTemp;
        //			double min = crossAxis._scale._minLinTemp;
        var max = crossAxisScale.Linearize (crossAxisScale._min);
        var min = crossAxisScale.Linearize (crossAxisScale._max);
        float frac;

        var scale = Scale.ThrowIfNull();
        if ((this is XAxis || this is YAxis) && scale._isLabelsInside == crossAxisScale.IsReverse ||
            (this is X2Axis || this is Y2Axis) && scale._isLabelsInside != crossAxisScale.IsReverse)
        {
            frac = (float)((effCross - min) / (max - min));
        }
        else
        {
            frac = (float)((max - effCross) / (max - min));
        }

        if (frac < 0.0f)
        {
            frac = 0.0f;
        }

        if (frac > 1.0f)
        {
            frac = 1.0f;
        }

        return frac;
    }

    private float CalcTotalShift
        (
            GraphPane pane,
            float scaleFactor,
            float shiftPos
        )
    {
        if (!IsPrimary (pane))
        {
            // if ( CalcCrossFraction( pane ) != 0.0 )
            if (IsCrossShifted (pane))
            {
                shiftPos = 0;
            }
            else
            {
                // Scaled size (pixels) of a tic
                var ticSize = _majorTic.ScaledTic (scaleFactor);

                // if the scalelabels are on the inside, shift everything so the axis is drawn,
                // for example, to the left side of the available space for a YAxis type
                if (Scale?._isLabelsInside ?? false)
                {
                    shiftPos += _tmpSpace;

                    // shift the axis to leave room for the outside tics
                    if (_majorTic.IsOutside || _majorTic._isCrossOutside ||
                        _minorTic.IsOutside || _minorTic._isCrossOutside)
                    {
                        shiftPos -= ticSize;
                    }
                }
                else
                {
                    // if it's not the primary axis, add a tic space for the spacing between axes
                    shiftPos += _axisGap * scaleFactor;

                    // if it has inside tics, leave another tic space
                    if (_majorTic.IsInside || _majorTic._isCrossInside ||
                        _minorTic.IsInside || _minorTic._isCrossInside)
                    {
                        shiftPos += ticSize;
                    }
                }
            }
        }

        // shift is the position of the actual axis line itself
        // everything else is based on that position.
        var crossShift = CalcCrossShift (pane);
        shiftPos += crossShift;

        return shiftPos;
    }

    /// <summary>
    /// Calculate the space required (pixels) for this <see cref="Axis"/> object.
    /// </summary>
    /// <remarks>
    /// This is the total space (vertical space for the X axis, horizontal space for
    /// the Y axes) required to contain the axis.  If <see cref="Cross" /> is zero, then
    /// this space will be the space required between the <see cref="Chart.Rect" /> and
    /// the <see cref="PaneBase.Rect" />.  This method sets the internal values of
    /// <see cref="_tmpSpace" /> for use by the <see cref="GraphPane.CalcChartRect(Graphics)" />
    /// method.
    /// </remarks>
    /// <param name="graphics">
    /// A graphic device object to be drawn into.  This is normally e.Graphics from the
    /// PaintEventArgs argument to the Paint() method.
    /// </param>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    /// <param name="scaleFactor">
    /// The scaling factor to be used for rendering objects.  This is calculated and
    /// passed down by the parent <see cref="GraphPane"/> object using the
    /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust
    /// font sizes, etc. according to the actual size of the graph.
    /// </param>
    /// <param name="fixedSpace">The amount of space (pixels) at the edge of the ChartRect
    /// that is always required for this axis, even if the axis is shifted by the
    /// <see cref="Cross" /> value.</param>
    /// <returns>Returns the space, in pixels, required for this axis (between the
    /// rect and ChartRect)</returns>
    public float CalcSpace
        (
            Graphics graphics,
            GraphPane pane,
            float scaleFactor,
            out float fixedSpace
        )
    {
        //fixedSpace = 0;

        //Typical character height for the scale font
        var charHeight = Scale?._fontSpec.GetHeight (scaleFactor) ?? 0;

        // Scaled size (pixels) of a tic
        var ticSize = _majorTic.ScaledTic (scaleFactor);

        // Scaled size (pixels) of the axis gap
        var axisGap = _axisGap * scaleFactor;
        var scaledLabelGap = Scale?._labelGap * charHeight ?? 0;
        var scaledTitleGap = Title.GetScaledGap (scaleFactor);

        // The minimum amount of space to reserve for the NORMAL position of the axis.  This would
        // be the left side of the chart rect for the Y axis, the right side for the Y2 axis, etc.
        // This amount of space is based on the need to reserve space for tics, etc., even if the
        // Axis.Cross property causes the axis to be in a different location.
        fixedSpace = 0;

        // The actual space needed for this axis (ignoring the setting of Axis.Cross)
        _tmpSpace = 0;

        // Account for the Axis
        if (IsVisible)
        {
            var hasTic = MajorTic.IsOutside || MajorTic._isCrossOutside ||
                         MinorTic.IsOutside || MinorTic._isCrossOutside;

            // account for the tic space.  Leave the tic space for any type of outside tic (Outside Tic Space)
            if (hasTic)
            {
                _tmpSpace += ticSize;
            }

            // if this is not the primary axis
            if (!IsPrimary (pane))
            {
                // always leave an extra tic space for the space between the multi-axes (Axis Gap)
                _tmpSpace += axisGap;

                // if it has inside tics, leave another tic space (Inside Tic Space)
                if (MajorTic._isInside || MajorTic._isCrossInside ||
                    MinorTic._isInside || MinorTic._isCrossInside)
                {
                    _tmpSpace += ticSize;
                }
            }

            // tic takes up 1x tic
            // space between tic and scale label is 0.5 tic
            // scale label is GetScaleMaxSpace()
            // space between scale label and axis label is 0.5 tic

            // account for the tic labels + 'LabelGap' tic gap between the tic and the label
            _tmpSpace += Scale.GetScaleMaxSpace (graphics, pane, scaleFactor, true).Height +
                         scaledLabelGap;

            var str = MakeTitle();

            // Only add space for the title if there is one
            // Axis Title gets actual height
            // if ( str.Length > 0 && _title._isVisible )
            if (!string.IsNullOrEmpty (str) && Title.IsVisible)
            {
                //tmpSpace += this.TitleFontSpec.BoundingBox( g, str, scaleFactor ).Height;
                fixedSpace = Title.FontSpec?.BoundingBox (graphics, str, scaleFactor).Height +
                             scaledTitleGap ?? 0;
                _tmpSpace += fixedSpace;

                fixedSpace += scaledTitleGap;
            }

            if (hasTic)
            {
                fixedSpace += ticSize;
            }
        }

        // for the Y axes, make sure that enough space is left to fit the first
        // and last X axis scale label
        if (IsPrimary (pane) && (
                this is YAxis && (
                    !pane.XAxis.Scale!._isSkipFirstLabel &&
                    !pane.XAxis.Scale._isReverse ||
                    !pane.XAxis.Scale._isSkipLastLabel &&
                    pane.XAxis.Scale._isReverse) ||
                this is Y2Axis && (
                    !pane.XAxis.Scale!._isSkipFirstLabel &&
                    pane.XAxis.Scale._isReverse ||
                    !pane.XAxis.Scale._isSkipLastLabel &&
                    !pane.XAxis.Scale._isReverse)) &&
            pane.XAxis.IsVisible && pane.XAxis.Scale._isVisible)
        {
            // half the width of the widest item, plus a gap of 1/2 the charheight
            var tmp = pane.XAxis.Scale.GetScaleMaxSpace (graphics, pane, scaleFactor, true).Width / 2.0F;

            //+ charHeight / 2.0F;
            //if ( tmp > tmpSpace )
            //	tmpSpace = tmp;

            fixedSpace = Math.Max (tmp, fixedSpace);
        }

        // Verify that the minSpace property was satisfied
        _tmpSpace = Math.Max (_tmpSpace, _minSpace * scaleFactor);

        fixedSpace = Math.Max (fixedSpace, _minSpace * scaleFactor);

        return _tmpSpace;
    }

    /// <summary>
    /// Determines if this <see cref="Axis" /> object is a "primary" one.
    /// </summary>
    /// <remarks>
    /// The primary axes are the <see cref="XAxis" /> (always), the first
    /// <see cref="YAxis" /> in the <see cref="GraphPane.YAxisList" />
    /// (<see cref="CurveItem.YAxisIndex" /> = 0),  and the first
    /// <see cref="Y2Axis" /> in the <see cref="GraphPane.Y2AxisList" />
    /// (<see cref="CurveItem.YAxisIndex" /> = 0).  Note that
    /// <see cref="GraphPane.YAxis" /> and <see cref="GraphPane.Y2Axis" />
    /// always reference the primary axes.
    /// </remarks>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    /// <returns>true for a primary <see cref="Axis" /> (for the <see cref="XAxis" />,
    /// this is always true), false otherwise</returns>
    internal abstract bool IsPrimary (GraphPane pane);

    internal void FixZeroLine
        (
            Graphics graphics,
            GraphPane pane,
            float scaleFactor,
            float left,
            float right
        )
    {
        // restore the zero line if needed (since the fill tends to cover it up)
        if (IsVisible && _majorGrid.IsZeroLine &&
            Scale!._min < 0.0 && Scale._max > 0.0)
        {
            var zeroPix = Scale.Transform (0.0);

            using (var zeroPen = new Pen (_color, pane.ScaledPenWidth (_majorGrid._penWidth, scaleFactor)))
            {
                graphics.DrawLine (zeroPen, left, zeroPix, right, zeroPix);

                //zeroPen.Dispose();
            }
        }
    }

    /// <summary>
    /// Draw the minor tic marks as required for this <see cref="Axis"/>.
    /// </summary>
    /// <param name="graphics">
    /// A graphic device object to be drawn into.  This is normally e.Graphics from the
    /// PaintEventArgs argument to the Paint() method.
    /// </param>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    /// <param name="baseVal">
    /// The scale value for the first major tic position.  This is the reference point
    /// for all other tic marks.
    /// </param>
    /// <param name="shift">The number of pixels to shift this axis, based on the
    /// value of <see cref="Cross"/>.  A positive value is into the ChartRect relative to
    /// the default axis position.</param>
    /// <param name="scaleFactor">
    /// The scaling factor to be used for rendering objects.  This is calculated and
    /// passed down by the parent <see cref="GraphPane"/> object using the
    /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust
    /// font sizes, etc. according to the actual size of the graph.
    /// </param>
    /// <param name="topPix">
    /// The pixel location of the far side of the ChartRect from this axis.
    /// This value is the ChartRect.Height for the XAxis, or the ChartRect.Width
    /// for the YAxis and Y2Axis.
    /// </param>
    public void DrawMinorTics
        (
            Graphics graphics,
            GraphPane pane,
            double baseVal,
            float shift,
            float scaleFactor,
            float topPix
        )
    {
        if ((MinorTic.IsOutside || MinorTic.IsOpposite || MinorTic.IsInside ||
             MinorTic._isCrossOutside || MinorTic._isCrossInside || _minorGrid._isVisible)
            && IsVisible)
        {
            double tMajor = Scale!._majorStep * Scale.MajorUnitMultiplier,
                tMinor = Scale._minorStep * Scale.MinorUnitMultiplier;

            if (Scale.IsLog || tMinor < tMajor)
            {
                var minorScaledTic = MinorTic.ScaledTic (scaleFactor);

                // Minor tics start at the minimum value and step all the way thru
                // the full scale.  This means that if the minor step size is not
                // an even division of the major step size, the minor tics won't
                // line up with all of the scale labels and major tics.
                double first = Scale._minLinTemp,
                    last = Scale._maxLinTemp;

                var dVal = first;

                var iTic = Scale.CalcMinorStart (baseVal);
                var MajorTic = 0;
                var majorVal = Scale.CalcMajorTicValue (baseVal, MajorTic);

                using (var pen = new Pen (_minorTic._color,
                           pane.ScaledPenWidth (MinorTic._penWidth, scaleFactor)))
                using (var minorGridPen = _minorGrid.GetPen (pane, scaleFactor))
                {
                    // Draw the minor tic marks
                    while (dVal < last && iTic < 5000)
                    {
                        // Calculate the scale value for the current tic
                        dVal = Scale.CalcMinorTicValue (baseVal, iTic);

                        // Maintain a value for the current major tic
                        if (dVal > majorVal)
                        {
                            majorVal = Scale.CalcMajorTicValue (baseVal, ++MajorTic);
                        }

                        // Make sure that the current value does not match up with a major tic
                        if ((Math.Abs (dVal) < 1e-20 && Math.Abs (dVal - majorVal) > 1e-20 ||
                             Math.Abs (dVal) > 1e-20 && Math.Abs ((dVal - majorVal) / dVal) > 1e-10) &&
                            dVal >= first && dVal <= last)
                        {
                            var pixVal = Scale.LocalTransform (dVal);

                            _minorGrid.Draw (graphics, minorGridPen, pixVal, topPix);

                            _minorTic.Draw (graphics, pane, pen, pixVal, topPix, shift, minorScaledTic);
                        }

                        iTic++;
                    }
                }
            }
        }
    }

    /// <summary>
    /// Draw the title for this <see cref="Axis"/>.
    /// </summary>
    /// <remarks>On entry, it is assumed that the
    /// graphics transform has been configured so that the origin is at the left side
    /// of this axis, and the axis is aligned along the X coordinate direction.
    /// </remarks>
    /// <param name="graphics">
    /// A graphic device object to be drawn into.  This is normally e.Graphics from the
    /// PaintEventArgs argument to the Paint() method.
    /// </param>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    /// <param name="shiftPos">The number of pixels to shift this axis, based on the
    /// value of <see cref="Cross"/>.  A positive value is into the ChartRect relative to
    /// the default axis position.</param>
    /// <param name="scaleFactor">
    /// The scaling factor to be used for rendering objects.  This is calculated and
    /// passed down by the parent <see cref="GraphPane"/> object using the
    /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust
    /// font sizes, etc. according to the actual size of the graph.
    /// </param>
    public void DrawTitle
        (
            Graphics graphics,
            GraphPane pane,
            float shiftPos,
            float scaleFactor
        )
    {
        var str = MakeTitle();

        // If the Axis is visible, draw the title
        //if ( _isVisible && _title._isVisible && str.Length > 0 )
        if (IsVisible && Title.IsVisible && !string.IsNullOrEmpty (str))
        {
            var hasTic = Scale!._isLabelsInside
                ? MajorTic.IsInside || MajorTic._isCrossInside ||
                  MinorTic.IsInside || MinorTic._isCrossInside
                : MajorTic.IsOutside || MajorTic._isCrossOutside || MinorTic.IsOutside ||
                  MinorTic._isCrossOutside;

            // Calculate the title position in screen coordinates
            var x = (Scale._maxPix - Scale._minPix) / 2;

            var scaledTic = MajorTic.ScaledTic (scaleFactor);
            var scaledLabelGap = Scale._fontSpec.GetHeight (scaleFactor) * Scale._labelGap;
            var scaledTitleGap = Title.GetScaledGap (scaleFactor);

            // The space for the scale labels is only reserved if the axis is not shifted due to the
            // cross value.  Note that this could be a problem if the axis is only shifted slightly,
            // since the scale value labels may overlap the axis title.  However, it's not possible to
            // calculate that actual shift amount at this point, because the ChartRect rect has not yet been
            // calculated, and the cross value is determined using a transform of scale values (which
            // rely on ChartRect).

            var gap = scaledTic * (hasTic ? 1.0f : 0.0f) +
                      Title.FontSpec!.BoundingBox (graphics, str, scaleFactor).Height / 2.0F;
            var y = Scale._isVisible
                ? Scale.GetScaleMaxSpace (graphics, pane, scaleFactor, true).Height
                  + scaledLabelGap
                : 0;

            if (Scale._isLabelsInside)
            {
                y = shiftPos - y - gap;
            }
            else
            {
                y = shiftPos + y + gap;
            }

            if (!CrossAuto && !Title.IsTitleAtCross)
            {
                y = Math.Max (y, gap);
            }

            var alignV = AlignV.Center;

            // Add in the TitleGap space
            y += scaledTitleGap;

            // Draw the title
            Title.FontSpec.Draw (graphics, pane, str, x, y,
                AlignH.Center, alignV, scaleFactor);
        }
    }

    private string MakeTitle()
    {
        Title.Text ??= string.Empty;

        // Allow customization of the modified title when the scale is very large
        // The event handler can edit the full label.  If the handler returns
        // null, then the title will be the default.
        if (ScaleTitleEvent != null)
        {
            var label = ScaleTitleEvent (this);
            if (label != null)
            {
                return label;
            }
        }

        // If the Mag is non-zero and IsOmitMag == false, and IsLog == false,
        // then add the mag indicator to the title.
        if (Scale!._mag != 0 && !Title.IsOmitMag && !Scale.IsLog)
        {
            return Title.Text + $" (10^{Scale._mag})";
        }

        return Title.Text;
    }

    /// <summary>
    /// Make a value label for the axis at the specified ordinal position.
    /// </summary>
    /// <remarks>
    /// This method properly accounts for <see cref="Charting.Scale.IsLog"/>,
    /// <see cref="Charting.Scale.IsText"/>,
    /// and other axis format settings.  It also implements the ScaleFormatEvent such that
    /// custom labels can be created.
    /// </remarks>
    /// <param name="pane">
    /// A reference to the <see cref="GraphPane"/> object that is the parent or
    /// owner of this object.
    /// </param>
    /// <param name="index">
    /// The zero-based, ordinal index of the label to be generated.  For example, a value of 2 would
    /// cause the third value label on the axis to be generated.
    /// </param>
    /// <param name="dVal">
    /// The numeric value associated with the label.  This value is ignored for log
    /// (<see cref="Charting.Scale.IsLog"/>)
    /// and text (<see cref="Charting.Scale.IsText"/>) type axes.
    /// </param>
    /// <returns>The resulting value label as a <see cref="string" /></returns>
    internal string MakeLabelEventWorks
        (
            GraphPane pane,
            int index,
            double dVal
        )
    {
        // if there is a valid ScaleFormatEvent, then try to use it to create the label
        // the label will be non-null if it's to be used
        var label = ScaleFormatEvent?.Invoke (pane, this, dVal, index);
        if (label is not null)
        {
            return label;
        }

        // second try.  If there's no custom ScaleFormatEvent, then just call
        // _scale.MakeLabel according to the type of scale
        if (Scale != null)
        {
            return Scale.MakeLabel (pane, index, dVal);
        }

        return "?";
    }

    #endregion
}
